package math.dataset;

import java.util.Arrays;

/**
 * Useful class for doing simple statistical analysis of data.
 * Most technical details of statistical calculations is in this class.
 * Subclasses for the most part do not perform the calculation, but rather
 * call on this class' methods.
 * 
 * @author Shimu Wu
 */
public class DataSet_Float extends DataSet {

    /**
     * DataSet should not be called, only subclasses have
     * public constructors.
     */
    protected DataSet_Float() {
    }

    /**
     * Returns the mean (average) of the given data, null if the data is empty.
     * This method adds up all the value of the data, then divide by the
     * total number of elements.
     *
     * @param data
     * @return the mean (average) of the given data, null if the data is empty
     */
    protected Float getMean(float[] data) {
        if (data.length == 0) {
            return null;
        }
        float sum = 0;
        for (float value : data) {
            sum += value;
        }
        return (sum / data.length);
    }

    /**
     * Sorts the dataset in ascending order (smallest to biggest).
     */
    private void sortData(boolean isSorted, float[] data) {
        if (!isSorted) {
            Arrays.sort(data);
        }
    }

    /**
     * Returns the median (middle number) of the given data, null if the
     * data is empty. The method will sort the given data in ascending order
     * if isSorted is false, then it will return the number at index
     * n/2 if data size is odd (where n is the size of the data). 
     * If data size is odd, then it will return the average of the number 
     * at index floor(n/2) and the number at index floor(n/2) + 1
     *
     * @param isSorted true if the data is sorted in ascending order, false
     * otherwise.
     * @param data
     * @return the median (middle number) of the given data, null if the data
     * is empty.
     */
    protected Float getMedian(boolean isSorted, float[] data) {
        int size = data.length;
        if (size == 0) {
            return null;
        }
        sortData(isSorted, data);
        if (size % 2 == 1) { // If the dataset contains an odd number of values
            return data[size / 2];
        } else { // If the dataset contains an even number of values
            return (data[size / 2 - 1] + data[size / 2]) / 2.0f;
        }
    }

    /**
     * Returns the minimum value in the data by checking each element in the
     * data set. Runs in O(n), where n is the number of elements in the data.
     *
     * @param data
     * @return the minimum value of the data, null if the data is empty
     */
    protected Float getMin(float[] data) {
        if (data.length == 0) {
            return null;
        }
        float min = data[0];
        for (int i = 1; i < data.length; i++) {
            if (data[i] < min) {
                min = data[i];
            }
        }
        return min;
    }

    /**
     * Returns the minimum value in the data, returns null if the data is
     * empty. The method begin by checking if the data is already sorted in
     * ascending order, if so, return the first element in the data, if not,
     * sort the data in ascending order before returning the first element.
     * Runs in O(1) if the data is already sorted. Runs in O(n log n) if
     * the data is not sorted. This function is useful if the data is
     * already sorted, or if the data will need to be sorted later on anyways,
     * such as for finding the median.
     *
     * @param isSorted true if the given data is already sorted in ascending
     * order. False otherwise.
     * @param data
     * @return the minimum value of the data, null if the data is empty
     */
    protected Float getMin(boolean isSorted, float[] data) {
        if (data.length == 0) {
            return null;
        }
        sortData(isSorted, data);
        return data[0];
    }

    /**
     * Returns the maximum value in the data by checking each element in the
     * data set. Runs in O(n), where n is the number of elements in the data.
     *
     * @param data
     * @return the maximum value of the data, null if the data is empty
     */
    protected Float getMax(float[] data) {
        if (data.length == 0) {
            return null;
        }

        float max = data[0];
        for (int i = 1; i < data.length; i++) {
            if (data[i] > max) {
                max = data[i];
            }
        }
        return max;
    }

    /**
     * Returns the maximum value in the data, returns null if the data is
     * empty. The method begin by checking if the data is already sorted in
     * ascending order, if so, return the last element in the data, if not,
     * sort the data in ascending order before returning the last element.
     * Runs in O(1) if the data is already sorted. Runs in O(n log n) if
     * the data is not sorted. This function is useful if the data is
     * already sorted, or if the data will need to be sorted later on anyways,
     * such as for finding the median.
     *
     * @param isSorted true if the given data is already sorted in ascending
     * order. False otherwise.
     * @param data
     * @return the maximum value of the data, null if the data is empty
     */
    protected Float getMax(boolean isSorted, float[] data) {
        if (data.length == 0) {
            return null;
        }
        sortData(isSorted, data);
        return data[data.length - 1];
    }

    /**
     * Returns the variance of the given data. Returns null if the data is
     * empty. Returns 0 if the data contains only one value. Otherwise,
     * return the variance calculated using the following formula:
     * s^2 = [1/(n-1)]∑(xi - x_mean).
     *
     * @param data the data from which the variance is calculated
     * @param mean the mean of the data
     * @return the variance of the given data.
     */
    protected Float getVariance(float[] data, Float mean) {
        if ((data.length == 0) || (mean == null)) {
            return null;
        } else if (data.length == 1) {
            return 0f;
        }
        float sum = 0;
        for (float value : data) {
            sum += Math.pow(value - mean, 2);
        }
        return sum / (data.length - 1);
    }

    /**
     * Returns the standard deviation of the given data. The standard deviation
     * is calculated by calling getVariance(float[], float) and taking the
     * square root of the result. Returns null if the data is empty.
     * Returns 0 if the data contains only one value.
     *
     * @see getVariance(float[], float)
     * @param variance the variance of the given data
     * @return the standard deviation of the given data, null if the data is
     * empty, 0 if the data contains only one value.
     */
    protected Float getStandardDeviation(Float variance) {
        if (variance == null) {
            return null;
        }
        return new Float(Math.sqrt(variance));
    }

    /**
     * Returns the z-score of the given value, null if any of the parameters
     * are null, 0 if the standardDeviation is 0. The z-score is calculated
     * using the following formula:
     * z = (x - mean)/standardDeviation
     *
     * @param value
     * @param mean the mean of the dataset that the given value belongs to
     * @param standardDeviation the standard deviation of the dataset that
     * the given value belongs to
     * @return the z-score of the given value, null if any of the parameters
     * are null, 0 if the standardDeviation is 0.
     */
    protected Float getZScore(Float value, Float mean,
            Float standardDeviation) {
        if ((value == null) || (mean == null) || (standardDeviation == null)) {
            return null;
        } else if (standardDeviation == 0){
            return 0f;
        }
        return (value - mean) / standardDeviation;
    }
}
